using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using HttpStatusCode = System.Net.HttpStatusCode;
using Microsoft.Samples.ServiceHosting.StorageClient;
using Log = Microsoft.Samples.ServiceHosting.HelloFabric.ApplicationEnvironment;

namespace KlearStorage
{
    public class TablePersister : IPersister
    {
        private readonly StorageAccountInfo _account;
        private readonly TableStorage _tableStorage;

        private const int NumRetries = 3;
        private readonly RetryPolicy _tableRetry = RetryPolicies.RetryN(NumRetries, TimeSpan.FromSeconds(1));

        private readonly List<string> _createdTables = new List<string>();


        public TablePersister() : this(null)
        {
        }

        public TablePersister(StorageAccountInfo account)
        {
            try
            {
                _account = account ?? StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration();
                _tableStorage = TableStorage.Create(_account);

                // TODO: This doesn't work for fresh installs; probably because the AspProviders cannot be initialized
                // TryInitializeTables(); // TODO: This is quite slow and might cause the server to fail to start
            }
            catch (Exception e)
            {
                Log.LogError("Initialization of storage failed!");
                throw new Exception("Initialization of storage failed!", e);
            }
        }

        /*private void TryInitializeTables()
        {
            // TODO: Find a way to detect when initialization is needed

            Log.LogWarning("New installation. Initialize tables...");

            Azure.Setup.CreateTables(_account); // Note: This only works online
        }*/

        private void TryCreateTable(TableStorage storage, string name) // TODO: Move to a Initialization module to run only once?
        {
            if (_createdTables.Contains(name))
                return;
            if (storage.DoesTableExist(name) || storage.TryCreateTable(name)) // Do not add to the list if TryCreateTable() fails!
                _createdTables.Add(name);
        }

        private TableStorageDataServiceContext CreateDataServiceContext(string tableName)
        {
            TryCreateTable(_tableStorage, tableName); // TODO: Probably no longer needed since we have Setup
            var svc = _tableStorage.GetDataServiceContext();
            svc.MergeOption = MergeOption.NoTracking; // TODO
            return svc;
        }

        public static string GetTableName<T>()
        {
            return typeof(T).Name;
        }




        public TRow SelectById<TRow>(Guid id)
            where TRow : UniversalEntity
        {
            return SelectByPartitionKey<TRow>(id.ToString());
        }

        public TRow SelectByPartitionKey<TRow>(string key)
            where TRow : TableStorageEntity
        {
            return SelectSingleOrDefault<TRow>(
                q => q.Where(e => e.PartitionKey == key));
        }

        public TRow SelectByRowKey<TRow>(string key)
            where TRow : TableStorageEntity
        {
            return SelectSingleOrDefault<TRow>(
                q => q.Where(e => e.RowKey == key));
        }

        public TRow SelectByKey<TRow>(string partitionKey, string rowKey)
            where TRow : TableStorageEntity
        {
            return SelectSingleOrDefault<TRow>(
                q => q.Where(e => e.PartitionKey == partitionKey && e.RowKey == rowKey));
        }

        public TRow SelectSingleOrDefault<TRow>(Func<IQueryable<TRow>, IQueryable<TRow>> whereClause)
            where TRow : TableStorageEntity
        {
            var tableName = GetTableName<TRow>();
            var svc = CreateDataServiceContext(tableName);
            return Select(whereClause, tableName, svc).SingleOrDefault();
        }


        public IList<TRow> SelectAll<TRow>()
            where TRow : TableStorageEntity
        {
            return Select<TRow>(null);
        }

        public IList<TRow> Select<TRow>(Func<IQueryable<TRow>, IQueryable<TRow>> whereClause)
            where TRow : TableStorageEntity
        {
            var tableName = GetTableName<TRow>();
            var svc = CreateDataServiceContext(tableName);
            return Select(whereClause, tableName, svc).ToList();
        }


        private IEnumerable<TRow> Select<TRow>(Func<IQueryable<TRow>, IQueryable<TRow>> whereClause, string tableName, DataServiceContext svc)
            where TRow : TableStorageEntity
        {
            try
            {
                var query = from e in svc.CreateQuery<TRow>(tableName)
                            select e;
                if (whereClause != null)
                    query = whereClause(query);

                var q = new TableStorageDataServiceQuery<TRow>(query as DataServiceQuery<TRow>, _tableRetry);
                return q.ExecuteAllWithRetries();
            }
            catch (Exception ex)
            {
                return Log.LogErrorAndThrowException<List<TRow>>(ex, "Failed to process {0}#{1}.", tableName, whereClause);
            }
        }


        public void Insert<TRow>(params TRow[] rows)
            where TRow : TableStorageEntity
        {
            Insert((IList<TRow>)rows);
        }

        public void Insert<TRow>(IList<TRow> rows)
            where TRow : TableStorageEntity
        {
            if (rows.Count == 0)
                return; // Nothing to do
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);
                foreach (var e in rows)
                    svc.AddObject(tableName, e);
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just return success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
        }


        public void Update<TRow>(params TRow[] rows)
            where TRow : TableStorageEntity
        {
            Update((IList<TRow>)rows);
        }

        public void Update<TRow>(IList<TRow> rows)
            where TRow : TableStorageEntity
        {
            if (rows.Count == 0)
                return; // Nothing to do
            var tableName = GetTableName<TRow>();
            try
            {
                var svc = CreateDataServiceContext(tableName);
                foreach (var e in rows)
                {
                    svc.AttachTo(tableName, e, "*"); // TODO: This disables optimistic concurrency
                    svc.UpdateObject(e);
                }
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just return success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
        }


        public void InsertOrUpdate<TRow>(params TRow[] rows)
            where TRow : TableStorageEntity
        {
            Insert((IList<TRow>)rows);
        }

        public void InsertOrUpdate<TRow>(IList<TRow> rows)
            where TRow : TableStorageEntity
        {
            if (rows.Count == 0)
                return; // Nothing to do
            var tableName = GetTableName<TRow>();
            var svc = CreateDataServiceContext(tableName);
            try
            {
                foreach (var e in rows)
                    InsertOrUpdate(e, tableName, svc);
                svc.SaveChanges();
            }
            catch (InvalidOperationException ex)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just return success here
                if (TableStorageHelpers.EvaluateException(ex, out status) && status == HttpStatusCode.Conflict)
                    return;

                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}s.Count={1}.", tableName, rows.Count);
            }
        }

        private void InsertOrUpdate<TRow>(TRow row, string tableName, DataServiceContext svc)
            where TRow : TableStorageEntity
        {
            var persistedRow = Select<TRow>(
                q => q.Where(e => e.PartitionKey == row.PartitionKey && e.RowKey == row.RowKey), tableName, svc);
            if (persistedRow == null)
                svc.AddObject(tableName, row);
            else
            {
                svc.AttachTo(tableName, row, "*"); // TODO: This disables optimistic concurrency
                svc.UpdateObject(row);
            }
        }


        public void DeleteAll<TRow>()
            where TRow : TableStorageEntity
        {
            Delete<TRow>(null, false);
        }

        public void Delete<TRow>(TRow row)
            where TRow : TableStorageEntity
        {
            Delete<TRow>(
                q => q.Where(e => e.PartitionKey == row.PartitionKey && e.RowKey == row.RowKey), true);
        }

        public void Delete<TRow>(Func<IQueryable<TRow>, IQueryable<TRow>> whereClause, bool single)
            where TRow : TableStorageEntity
        {
            var tableName = GetTableName<TRow>();
            var svc = CreateDataServiceContext(tableName);
            IEnumerable<TRow> rows = Select(whereClause, tableName, svc);

            try
            {
                int count = 0;
                foreach (var r in rows)
                {
                    if (single && count++ > 1)
                        Log.LogWarning("There are multiple rows with the same key {0}#{1}. PartitionKey+RowKey should be unique.", tableName, whereClause);

                    svc.AttachTo(tableName, r, "*"); // TODO: This disables optimistic concurrency (should be ok since we just selected these entities)
                    svc.DeleteObject(r);
                }
                svc.SaveChanges();

                if (single && count == 0)
                    Log.LogWarning("There are no row to delete with the key {0}#{1}.", tableName, whereClause);
            }
            catch (Exception ex)
            {
                Log.LogErrorAndThrowException(ex, "Failed to process {0}#{1}.", tableName, whereClause);
            }
        }
    }
}